/*
 * sslbus.c
 *
 *  Created on: 08/mar/2011
 *      Author: ivano
 */

#include <stdlib.h>
#include <memory.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <openssl/ssl.h>
#include <sys/errno.h>
#include <pthread.h>

#include "../mod_general.h"
#include "../terminals.h"
#include "../list.h"

SSL_METHOD* ssl_method;
SSL_CTX* ssl_ctx;
SSL* ssl;

int listen_socket;
list* ssl_socks;
list* out_of_transaction;

typedef struct {
	SSL* ssl;
	terminal_t* term;
} connection_t;

void manage_transaction(terminal_t* daemon, terminal_t* terminal) {
	int in_transaction = 0;
	char* string;
	
	do {
		if (in_transaction)
			free(string);
		string = terminal_read_from_daemon (daemon);
		if (!strcmp(string, "transaction begin"))
			in_transaction = 1;
		if (in_transaction == 0)
			list_add(out_of_transaction, string, 0);
		else
			if (terminal_write_to_terminal (terminal, string) <= 0 && strlen(string) > 0) {
				free(string);
				return;
			}
	} while (strcmp(string, "transaction end"));
	free(string);
}

void* sslbus_thread(void* arg) {
	terminal_t* term = (terminal_t*) arg;
	int peer_addr_size, res, max, jump_select;
	struct sockaddr_in peer_addr;
	fd_set fds;
	char buffer[4096], *string;
	connection_t* conn_sock;

	peer_addr_size = sizeof(peer_addr);

	while (1) {
		max = term->pins.terminal_read;

		jump_select = 0;

		for (res = list_size(ssl_socks) - 1; res >= 0; res--) {
			conn_sock = (connection_t*) list_get(ssl_socks, res);
			if (conn_sock == NULL) {
				fprintf(stderr, "BUG: sslbus: list_get(socks, %d) returns null\n", res);
				continue;
			}
			if (terminal_has_more_for_daemon (conn_sock->term))
				jump_select = 1;
		}
		if (terminal_has_more_for_terminal (term))
			jump_select = 1;

		FD_ZERO(&fds);
		if (!jump_select) {
			FD_SET(listen_socket, &fds);
			FD_SET(term->pins.terminal_read, &fds);

			if (listen_socket > max)
				max = listen_socket;
	
			res = list_size(ssl_socks) - 1;
			for (; res >= 0; res--) {
				conn_sock = (connection_t*) list_get(ssl_socks, res);
				FD_SET(conn_sock->term->pins.daemon_read, &fds);
				if (conn_sock->term->pins.daemon_read > max)
					max = conn_sock->term->pins.daemon_read;
			}
			res = select (max + 1, &fds, NULL, NULL, NULL);
			if (res == -1) {
				fprintf(stderr, "ERROR: sslbus: select() failed: exiting\n");
				return NULL;
			}
		}
		
		// Accepting new connections
		if (FD_ISSET(listen_socket, &fds)) {
			conn_sock = (connection_t*) malloc(sizeof(connection_t));
			conn_sock->term = terminal_new_socket (accept(listen_socket, (struct sockaddr *) &peer_addr, &peer_addr_size));
			if (conn_sock->term->pins.daemon_read <= 0) {
				terminal_free (conn_sock->term);
				free(conn_sock);
				continue;
			}

			conn_sock->ssl = SSL_new(ssl_ctx);
			SSL_set_fd(conn_sock->ssl, conn_sock->term->pins.daemon_read);
			if (SSL_accept(conn_sock->ssl) != 1) {
				fprintf(stderr, "ERROR: sslbus: ssl handshake failed with client\n");
				ERR_print_errors_fp(stderr);
				SSL_shutdown(conn_sock->ssl);
				SSL_free(conn_sock->ssl);
				terminal_free(conn_sock->term);
				free(conn_sock);
				continue;
			}

			terminal_set_iofcnts (conn_sock->term, conn_sock->ssl, SSL_read, SSL_write);

			fprintf(stderr, "INFO: sslbus: client connected\n");

			//if (write(conn_sock, PRESENTATION, strlen(PRESENTATION)) <= 0)
			//	continue;

			list_add(ssl_socks, (void*) conn_sock, conn_sock->term->pins.daemon_read);
		}
		// Data from daemon to clients
		if (terminal_has_more_for_terminal(term) || FD_ISSET(term->pins.terminal_read, &fds)) {
			do {
				string = terminal_read_from_daemon(term);
				if (string == NULL)
					return NULL;
				if (strlen(string) == 0) {
					free(string);
					continue;
				}
				max = list_size(ssl_socks) - 1;
				for (; max >= 0; max--) {
					conn_sock = (connection_t*) list_get(ssl_socks, max);
					if (conn_sock == NULL) {
						fprintf(stderr, "BUG: sslbus: list_get returned NULL from list \"socks\" with index %d\n", max);
						continue;
					}
					if (terminal_write_to_terminal(conn_sock->term, string) <= 0 && strlen(string) > 0) {
						terminal_free(list_remove (ssl_socks, max));
					}
				}
				free(string);
			} while (terminal_has_more_for_terminal(term));
		}
		// Data from clients to daemon
		for (max = list_size(ssl_socks) - 1; max >= 0; max--) {
			conn_sock = (connection_t*) list_get(ssl_socks, max);
			if (conn_sock == NULL) {
				fprintf(stderr, "BUG: sslbus: list_get(socks, %d) returned NULL\n", max);
				continue;
			}
			if (!terminal_has_more_for_daemon(conn_sock->term) && !FD_ISSET(conn_sock->term->pins.daemon_read, &fds))
				continue;
			do {
				string = terminal_read_from_terminal(conn_sock->term);
				if (string == NULL) {
					fprintf(stderr, "INFO: sslbus: client disconnected (%d)\n", conn_sock->term->pins.daemon_read);
					break;
				}
				terminal_write_to_daemon(term, string);
				if (!strncmp(string, "transaction ", 12))
					manage_transaction(term, conn_sock->term);
				free(string);
			} while (string != NULL && terminal_has_more_for_daemon(conn_sock->term));
			if (string == NULL) {
				//close(usr->pins.daemon_read);
				if (list_remove_key(ssl_socks, conn_sock->term->pins.daemon_read) == NULL)
					fprintf(stderr, "BUG: sslbus: unable to find socket to be removed\n");
				else {
					SSL_free(conn_sock->ssl);
					terminal_free(conn_sock->term);
					free(conn_sock);
				}
			}
		}
		// Data out of transaction from daemon to clients
		while(list_size (out_of_transaction) > 0) {
			string = (char*) list_remove(out_of_transaction, 0);
			if (strlen(string) == 0) {
				free(string);
				continue;
			}
			max = list_size(ssl_socks) - 1;
			for (; max >= 0; max--) {
				conn_sock = (terminal_t*) list_get(ssl_socks, max);
				if (conn_sock == NULL) {
					fprintf(stderr, "BUG: sslbus: list_get returned NULL from \"socks\" with index %d\n", max);
					continue;
				}
				if (terminal_write_to_terminal(conn_sock, string) <= 0 && strlen(string) > 0) {
					terminal_free(list_remove (ssl_socks, max));
				}
			}
			free(string);
		}

	}

}

int module_init(terminal_t* term) {
	char* str;
	struct sockaddr_in sa_serv;
	int res, port;
	pthread_t tid;

	fprintf(stderr, "INFO: sslbus: initializing SSL support\n");

	SSL_library_init(); /* load encryption & hash algorithms for SSL */
	SSL_load_error_strings(); /* load the error strings for good error reporting */

	// Setting SSL context
	ssl_method = TLSv1_server_method();
	ssl_ctx = SSL_CTX_new(ssl_method);

	// Loading server certificate
	fprintf(stderr, "INFO: sslbus: loading server certificate\n");
	str = getenv("CASPER_SSL_CERTIFICATES_FILE");
	if (str == NULL) {
		fprintf(stderr, "ERROR: sslbus: unable to find CASPER_SSL_CERTIFICATES_FILE variable\n");
		return 1;
	}
	if (!SSL_CTX_use_certificate_file(ssl_ctx, str, SSL_FILETYPE_PEM)) {
		fprintf(stderr, "ERROR: sslbus: unable to use the specified certificate: %s\n", str);
		ERR_print_errors_fp(stderr);
		return 1;
	}

	// Loading private key
	fprintf(stderr, "INFO: sslbus: loading server private key\n");
	str = getenv("CASPER_SSL_PKEY_FILE");
	if (str == NULL) {
		fprintf(stderr, "ERROR: sslbus: unable to find CASPER_SSL_PKEY_FILE variable\n");
		return 1;
	}
	if (!SSL_CTX_use_RSAPrivateKey_file(ssl_ctx, str, SSL_FILETYPE_PEM)) {
		fprintf(stderr, "ERROR: sslbus: unable to load the specified private key: %s\n", str);
		ERR_print_errors_fp(stderr);
		return 1;
	}

	// Setting peer verification on
	SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);

	fprintf(stderr, "INFO: sslbus: loading trusted CA certificates\n");
	str = getenv("CASPER_SSL_CACERT");
	if (str == NULL) {
		fprintf(stderr, "ERROR: sslbus: unable find CASPER_SSL_CACERT variable\n");
		ERR_print_errors_fp(stderr);
		return 1;
	}
	if (SSL_CTX_load_verify_locations(ssl_ctx, str, NULL) != 1) {
		fprintf(stderr, "ERROR: sslbus: unable to read trusted CA certificates from %s\n", str);
		ERR_print_errors_fp(stderr);
		return 1;
	}

	// Creating the SSL structure
	ssl = SSL_new(ssl_ctx);
	if (ssl == NULL) {
		fprintf(stderr, "ERROR: sslbus: unable to create the SSL structure\n");
		ERR_print_errors_fp(stderr);
		return 1;
	}

	//////////////////////////////////////
	// OPENING THE LISTENING TCP SOCKET //
	//////////////////////////////////////

	str = getenv("CASPER_SSL_PORT");
	if (str == NULL) {
		fprintf(stderr, "ERROR: sslbus: unable to find CASPER_SSL_PORT variable\n");
		return 1;
	}
	if (sscanf(str, "%d", &port) != 1) {
		fprintf(stderr, "ERROR: sslbus: unable to read port number from CASPER_SSL_PORT variable\n");
		return 1;
	}

	fprintf(stderr, "INFO: sslbus: creating TCP socket listening on port %d\n", port);

	listen_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (listen_socket < 0) {
		fprintf(stderr, "ERROR: sslbus: unable to create TCP/IP socket (errno: %d)\n", errno);
		fprintf(stderr, "ERROR: -> %s\n", strerror(errno));
		return 1;
	}

	res = 1;
	setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, &res, sizeof(res));

	memset(&sa_serv, 0, sizeof(sa_serv));
	sa_serv.sin_family      = AF_INET;
	sa_serv.sin_addr.s_addr = INADDR_ANY;
	sa_serv.sin_port        = htons(port);      /* Server Port number */

	if (bind(listen_socket, (struct sockaddr*) &sa_serv,sizeof(sa_serv)) < 0) {
		fprintf(stderr, "ERROR: sslbus: unable to bind TCP/IP socket (errno: %d)\n", errno);
		fprintf(stderr, "ERROR: -> %s\n", strerror(errno));
		return 1;
	}

	/* Receive a TCP connection. */
	if (listen(listen_socket, 5) < 0) {
		fprintf(stderr, "ERROR: sslbus: unable to listen on TCP/IP socket (errno: %d)\n", errno);
		fprintf(stderr, "ERROR: -> %s\n", strerror(errno));
		return 1;
	}

	// Initializing sockets list
	ssl_socks = list_new();
	if (ssl_socks == NULL) {
		fprintf(stderr, "ERROR: sslbus: unable to create sockets list\n");
		return 1;
	}

	/////////////////////////////
	// CREATING WORKING THREAD //
	/////////////////////////////
	fprintf(stderr, "INFO: sslbus: creating working thread\n");

	// Starting thread
	res = pthread_create(&tid, NULL, sslbus_thread, term);
	if (res != 0) {
		fprintf(stderr, "ERROR: sslbus: unable to create thread (pthread_create: %d)\n", res);
		fprintf(stderr, "ERROR: -> %s\n", strerror(errno));
		return errno;
	}

	return 0;
}
